<?php

namespace Yolo\Service;

use GuzzleHttp\Exception\ClientException;
use Hyperf\Contract\OnRequestInterface;
use ReflectionException;
use Swoole\Http\Request;
use Swoole\Http\Response;
use Throwable;
use Yolo\Components\Middleware\MiddlewareManager;
use Yolo\Components\Validator\ValidatorFactory;
use Yolo\Di\DI;
use Yolo\Di\Errors\CircularDependencyException;
use Yolo\Di\Errors\InvalidAttributeException;
use Yolo\Di\Errors\ParameterTypeEmptyException;
use Yolo\Errors\EndpointNotFoundException;
use Yolo\Errors\InvalidParameterException;
use Yolo\Errors\InvalidPreProcessorCallException;
use Yolo\Errors\InvalidResponseException;
use Yolo\Errors\MethodNotAllowedException;
use Yolo\Errors\RpcException;

class OnRequest implements OnRequestInterface
{
    private MiddlewareManager $middlewareManager;

    public function __construct()
    {
        // 此类为收到请求的入口，不是使用DI::use()方法，而是直接实例化，所以无法使用依赖注入
        $this->middlewareManager = DI::use(MiddlewareManager::class);
    }

    /**
     * 请求入口
     * @param Request $request
     * @param Response $response
     * @return void
     */
    public function onRequest($request, $response): void
    {
        $start = $request->server['request_time_float'] ?? microtime(true);

        $isOk = true;

        $requestContent = $request->getContent();
        $args = json_decode($requestContent, true);

        $rpcRequest = new RpcRequest($request, $args);
        $rpcResponse = new RpcResponse($response);

        // 执行请求之前的中间件
        [$rpcRequest, $rpcResponse] = $this->middlewareManager->runBeforeRequestHandle($rpcRequest, $rpcResponse);

        try {

            if ($request->getMethod() !== 'POST') {
                throw rpc_error(new MethodNotAllowedException(), [
                    'method' => $request->getMethod(),
                ]);
            }

            $rpcResponse = $this->handleRequest($rpcRequest, $rpcResponse);

        } catch (Throwable $exception) {

            $isOk = false;

            $rpcResponse = $this->handleException($exception, $rpcResponse);

        } finally {

            // 执行请求之后的中间件
            [, $rpcResponse] = $this->middlewareManager->runAfterRequestHandled($rpcRequest, $rpcResponse);

            $end = microtime(true);

            logInfo([
                'action' => 'request',
                'message' => $isOk ? 'ok' : 'nok',
                'data' => [
                    'duration' => round(($end - $start) * 1000000),
                    'api' => $rpcRequest->getEndpoint(),
                ]
            ]);

            foreach ($rpcResponse->getHeaders() as $name => $value) {
                $response->header($name, $value);
            }

            $response->status($rpcResponse->getStatusCode());
            $response->end($rpcResponse->getBody());

            return;
        }
    }

    /**
     * Handle the request
     * @param RpcRequest $request
     * @param RpcResponse $response
     * @return RpcResponse
     * @throws CircularDependencyException
     * @throws EndpointNotFoundException
     * @throws InvalidAttributeException
     * @throws InvalidParameterException
     * @throws ParameterTypeEmptyException
     * @throws ReflectionException|InvalidResponseException|InvalidPreProcessorCallException
     */
    private function handleRequest(RpcRequest $request, RpcResponse $response): RpcResponse
    {
        $apiManager = DI::use(RpcApiManager::class);
        $apiInfo = $apiManager->getEndpoint($request->getEndpoint());

        if (!empty($apiInfo['options']['validation'])) {
            $this->validateRequest($request, $apiInfo['type'], $apiInfo['options']['validation']);
        }

        if (isset($apiInfo['options']['preProcessor'])) {
            [$request, $response] = $this->handlePreProcessor($apiInfo['options']['preProcessor'], $request, $response);
        }

        $controller = DI::use($apiInfo['invoke']['controller']);
        $action = $apiInfo['invoke']['action'];
        $result = $controller->$action($request, $response);

        if (is_object($result)) {
            if ($result instanceof RpcResponse) {
                return $result;
            } else {
                throw new InvalidResponseException();
            }
        } else {

            if (is_array($result)) {
                $response->setBody(json_encode($result));
            } elseif (is_string($result)) {
                $response->setBody($result);
            } else {
                throw new InvalidResponseException();
            }
        }

        return $response;
    }

    /**
     * Validate the request parameters
     * @param RpcRequest $request
     * @param string $apiType
     * @param array $rules
     * @return void
     * @throws CircularDependencyException
     * @throws InvalidAttributeException
     * @throws InvalidParameterException
     * @throws ParameterTypeEmptyException
     * @throws ReflectionException
     */
    private function validateRequest(RpcRequest $request, string $apiType, array $rules): void
    {
        $data = [
            'body' => $request->getBody(),
            'path' => $request->getPathArguments(),
            'qs' => $request->getQueryArguments(),
        ];

        if ($apiType === 'Restful') {

            if (!empty($rules['body'])) {
                $this->validateData($rules['body'], $data['body']);
            }

            if (!empty($rules['path'])) {
                $this->validateData($rules['path'], $data['path']);
            }

            if (!empty($rules['qs'])) {
                $this->validateData($rules['qs'], $data['qs']);
            }
        } else {
            $this->validateData($rules, $data['body']);
        }
    }

    /**
     * @param array $rules
     * @param array $data
     * @return void
     * @throws CircularDependencyException
     * @throws InvalidAttributeException
     * @throws InvalidParameterException
     * @throws ParameterTypeEmptyException
     * @throws ReflectionException
     */
    private function validateData(array $rules, array $data): void
    {
        $validator = DI::use(ValidatorFactory::class);
        $messageBag = $validator->validate($rules, $data);
        if ($messageBag) {
            $e = new InvalidParameterException();
            $e->setErrors($messageBag);
            throw $e;
        }
    }

    /**
     * Handle the request pre processor
     * @param mixed $preProcessor
     * @param RpcRequest $request
     * @param RpcResponse $response
     * @return array
     * @throws CircularDependencyException
     * @throws InvalidAttributeException
     * @throws InvalidPreProcessorCallException
     * @throws ParameterTypeEmptyException
     * @throws ReflectionException
     */
    private function handlePreProcessor(mixed $preProcessor, RpcRequest $request, RpcResponse $response): array
    {
        if (is_callable($preProcessor)) {

            return call_user_func($preProcessor, $request, $response);
        } else {

            if (is_string($preProcessor)) {
                $preprocessorRegistry = DI::use(PreProcessorRegistry::class);
                $processor = $preprocessorRegistry->getProcessor($preProcessor);

                return $processor($request, $response);
            }

            throw new InvalidPreProcessorCallException();
        }
    }

    /**
     * Handle the exception
     * @param Throwable $throwable
     * @param RpcResponse $rpcResponse
     * @return RpcResponse
     */
    private function handleException(Throwable $throwable, RpcResponse $rpcResponse): RpcResponse
    {
        if ($throwable instanceof RpcException) {

            logError([
                'action' => 'request',
                'message' => $throwable->getMessage(),
                'data' => [
                    'error' => $throwable->json(),
                    'trace' => $throwable->getTrace(),
                ]
            ]);

            $json = $throwable->json();

        } elseif ($throwable instanceof ClientException) {

            $response = $throwable->getResponse();
            $contents  = $response->getBody()->getContents();

            $responseBody = json_decode($contents, true);
            if ($responseBody) {
                $json = $responseBody;
            } else {
                $json = [
                    'code' => -1,
                    'symbol' => 'system_error',
                    'message' => $throwable->getMessage() ?: 'Server Internal Error.',
                    'metadata' => [],
                    'http' => [
                        'status' => $response->getStatusCode(),
                    ]
                ];
            }

        } else {
            logError([
                'action' => 'request',
                'message' => $throwable->getMessage(),
                'data' => [
                    'error' => [
                        'code' => $throwable->getCode(),
                        'message' => $throwable->getMessage(),
                        'trace' => $throwable->getTrace(),
                    ]
                ]
            ]);

            $json = [
                'code' => -1,
                'symbol' => 'system_error',
                'message' => $throwable->getMessage() ?: 'Server Internal Error.',
                'metadata' => [],
                'http' => [
                    'status' => 500,
                ]
            ];
        }

        $rpcResponse->setStatusCode($json['http']['status']);
        $rpcResponse->setBody(json_encode($json));

        return $rpcResponse;
    }
}
